// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.AspNetCore.SignalR.Client.SourceGenerator
{
    internal partial class HubServerProxyGenerator
    {
        public class Emitter
        {
            private readonly SourceProductionContext _context;
            private readonly SourceGenerationSpec _spec;

            public Emitter(SourceProductionContext context, SourceGenerationSpec spec)
            {
                _context = context;
                _spec = spec;
            }

            public void Emit()
            {
                if (string.IsNullOrEmpty(_spec.GetterClassAccessibility) ||
                    string.IsNullOrEmpty(_spec.GetterMethodAccessibility) ||
                    string.IsNullOrEmpty(_spec.GetterClassName) ||
                    string.IsNullOrEmpty(_spec.GetterMethodName) ||
                    string.IsNullOrEmpty(_spec.GetterTypeParameterName) ||
                    string.IsNullOrEmpty(_spec.GetterHubConnectionParameterName))
                {
                    return;
                }

                // Generate extensions and other user facing mostly-static code in a single source file
                EmitExtensions();
                // Generate hub proxy code in its own source file for each hub proxy type
                foreach (var classSpec in _spec.Classes)
                {
                    EmitProxy(classSpec);
                }
            }

            private void EmitExtensions()
            {
                var getProxyBody = new StringBuilder();

                foreach (var classSpec in _spec.Classes)
                {
                    var fqIntfTypeName = classSpec.FullyQualifiedInterfaceTypeName;
                    var fqClassTypeName =
                        $"{_spec.GetterNamespace}.{_spec.GetterClassName}.{classSpec.ClassTypeName}";
                    getProxyBody.Append($@"
            if (typeof({_spec.GetterTypeParameterName}) == typeof({fqIntfTypeName}))
            {{
                return ({_spec.GetterTypeParameterName}) ({fqIntfTypeName}) new {fqClassTypeName}({_spec.GetterHubConnectionParameterName});
            }}");
                }

                var getProxy = $@"// <auto-generated>
// Generated by Microsoft.AspNetCore.Client.SourceGenerator
// </auto-generated>

#nullable enable

using Microsoft.AspNetCore.SignalR.Client;

namespace {_spec.GetterNamespace}
{{
    {_spec.GetterClassAccessibility} static partial class {_spec.GetterClassName}
    {{
        {_spec.GetterMethodAccessibility} static partial {_spec.GetterTypeParameterName} {_spec.GetterMethodName}<{_spec.GetterTypeParameterName}>(this HubConnection {_spec.GetterHubConnectionParameterName})
        {{
{getProxyBody.ToString()}
            throw new System.ArgumentException(nameof({_spec.GetterTypeParameterName}));
        }}
    }}
}}";

                _context.AddSource("HubServerProxy.g.cs", SourceText.From(getProxy.ToString(), Encoding.UTF8));
            }

            private void EmitProxy(ClassSpec classSpec)
            {
                var methods = new StringBuilder();

                foreach (var methodSpec in classSpec.Methods)
                {
                    var signature = new StringBuilder($"public {methodSpec.FullyQualifiedReturnTypeName} {methodSpec.Name}(");
                    var callArgs = new StringBuilder("");
                    var signatureArgs = new StringBuilder("");
                    var first = true;
                    foreach (var argumentSpec in methodSpec.Arguments)
                    {
                        if (!first)
                        {
                            signatureArgs.Append(", ");
                        }

                        first = false;
                        signatureArgs.Append($"{argumentSpec.FullyQualifiedTypeName} {argumentSpec.Name}");
                        callArgs.Append($", {argumentSpec.Name}");
                    }
                    signature.Append(signatureArgs);
                    signature.Append(')');

                    // Prepare method body
                    var body = "";
                    if (methodSpec.Support != SupportClassification.Supported)
                    {
                        body = methodSpec.SupportHint is null
                            ? "throw new System.NotSupportedException();"
                            : $"throw new System.NotSupportedException(\"{methodSpec.SupportHint}\");";
                    }
                    else
                    {
                        // Get specific hub connection extension method call
                        var specificCall = GetSpecificCall(methodSpec);

                        // Handle ValueTask
                        var prefix = "";
                        var suffix = "";
                        if (methodSpec.IsReturnTypeValueTask)
                        {
                            if (methodSpec.InnerReturnTypeName is not null)
                            {
                                prefix = $"new System.Threading.Tasks.ValueTask<{methodSpec.InnerReturnTypeName}>(";
                            }
                            else
                            {
                                prefix = "new System.Threading.Tasks.ValueTask(";
                            }
                            suffix = $")";
                        }

                        // Bake it all together
                        body = $"return {prefix}this.connection.{specificCall}(\"{methodSpec.Name}\"{callArgs}){suffix};";
                    }

                    var method = $@"
        {signature}
        {{
            {body}
        }}
";
                    methods.Append(method);
                }

                var proxy = $@"// <auto-generated>
// Generated by Microsoft.AspNetCore.Client.SourceGenerator
// </auto-generated>

#nullable enable

using Microsoft.AspNetCore.SignalR.Client;

namespace {_spec.GetterNamespace}
{{
    {_spec.GetterClassAccessibility} static partial class {_spec.GetterClassName}
    {{
        private sealed class {classSpec.ClassTypeName} : {classSpec.FullyQualifiedInterfaceTypeName}
        {{
            private readonly HubConnection connection;
            internal {classSpec.ClassTypeName}(HubConnection connection)
            {{
                this.connection = connection;
            }}
    {methods.ToString()}
        }}
    }}
}}";

                _context.AddSource($"HubServerProxy.{classSpec.ClassTypeName}.g.cs", SourceText.From(proxy.ToString(), Encoding.UTF8));
            }

            private string GetSpecificCall(MethodSpec methodSpec)
            {
                if (methodSpec.Stream.HasFlag(StreamSpec.ServerToClient) &&
                    !methodSpec.Stream.HasFlag(StreamSpec.AsyncEnumerable))
                {
                    return $"StreamAsChannelAsync<{methodSpec.InnerReturnTypeName}>";
                }

                if (methodSpec.Stream.HasFlag(StreamSpec.ServerToClient) &&
                    methodSpec.Stream.HasFlag(StreamSpec.AsyncEnumerable))
                {
                    return $"StreamAsync<{methodSpec.InnerReturnTypeName}>";
                }

                if (methodSpec.InnerReturnTypeName is not null)
                {
                    return $"InvokeAsync<{methodSpec.InnerReturnTypeName}>";
                }

                if (methodSpec.Stream.HasFlag(StreamSpec.ClientToServer))
                {
                    return "SendAsync";
                }

                return "InvokeAsync";
            }
        }
    }
}
